﻿//-----------------------------------------------------------------------
// <copyright file="Account.cs" company="FastNET Services">
//     Copyright (c) 2008-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
//-----------------------------------------------------------------------
#region Usings

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using GateShift.Common.Repository;
using GateShift.Common.Repository.Business;
using GateShift.Common.Repository.Interfaces.DataInterfaces;
using GateShift.Common.Repository.Interfaces.Factories;
using GateShift.Common.Repository.Interfaces.Repositories;

#endregion

namespace GateShift.Services.GameFramework.GameObjects
{
    /// <summary>
    /// Business class for managing all account related activity
    /// </summary>
    public class Account : IdentityBusinessBase<IAccountData, IAccountRepository>
    {
        #region CTor/DTor

        #region Creation Routines

        /// <summary>
        /// Initializes a new instance of the Account class
        /// </summary>
        public Account()
            : this(new RepositoryFactory())
        {
        }

        /// <summary>
        /// Initializes a new instance of the Account class
        /// </summary>
        /// <param name="factory">Factory for retrieving the Repository from</param>
        /// <exception cref="ArgumentNullException">Thrown if Factory is null</exception>
        public Account(IRepositoryFactory factory)
            : base(factory)
        {
            Data.DateCreated = DateTime.UtcNow;
            GenerateNewSalt();
        }

        #endregion

        #region Retrieve by ID

        /// <summary>
        /// Initializes a new instance of the Account class
        /// </summary>
        /// <param name="ID">Identity of the data to retrieve</param>
        /// <exception cref="ArgumentNullException">Thrown if ID is null</exception>
        public Account(Identity ID)
            : this(new RepositoryFactory(), ID)
        {
        }

        /// <summary>
        /// Initializes a new instance of the Account class
        /// </summary>
        /// <param name="factory">Factory for retrieving the Repository from</param>
        /// <param name="ID">Identity of the data to retrieve</param>
        /// <exception cref="ArgumentNullException">Thrown if Factory or ID are null</exception>
        public Account(IRepositoryFactory factory, Identity ID)
            : base(factory, ID)
        {
        }

        #endregion

        #region Retrieve by Data

        /// <summary>
        /// Initializes a new instance of the Account class
        /// </summary>
        /// <param name="factory">Factory for retrieving the Repository from</param>
        /// <param name="data">IAccountData of the data to retrieve</param>
        /// <exception cref="ArgumentNullException">Thrown if Factory or ID are null</exception>
        private Account(IRepositoryFactory factory, IAccountData data)
            : base(factory, data)
        {
        }

        #endregion

        #endregion

        #region Read Only Properties

        /// <summary>
        /// Gets the DateTime that the account was created
        /// </summary>
        public DateTime DateCreated
        {
            get
            {
                return Data.DateCreated;
            }
        }

        /// <summary>
        /// Gets the DateTime that the account last logged in
        /// </summary>
        public DateTime? DateLastLogin
        {
            get
            {
                return Data.DateLastLogin;
            }
        }

        /// <summary>
        /// Gets the DateTime that the account was last locked
        /// </summary>
        public DateTime? DateLastLocked
        {
            get
            {
                return Data.DateLastLocked;
            }
        }

        /// <summary>
        /// Gets the date the account is locked until
        /// </summary>
        public DateTime? DateLockedUntil
        {
            get
            {
                return Data.DateLockedUntil;
            }
        }

        /// <summary>
        /// Gets the DateTime that the account was last banned
        /// </summary>
        public DateTime? DateLastBanned
        {
            get
            {
                return Data.DateLastBanned;
            }
        }

        /// <summary>
        /// Gets the date the account is banned until
        /// </summary>
        public DateTime? DateBannedUntil
        {
            get
            {
                return Data.DateBannedUntil;
            }
        }

        /// <summary>
        /// Gets the date the password was last changed
        /// </summary>
        public DateTime? DateLastPasswordChanged
        {
            get
            {
                return Data.DateLastPasswordChanged;
            }
        }

        /// <summary>
        /// Gets the salt for password validation purposes
        /// </summary>
        public byte[] PasswordSalt
        {
            get
            {
                return Data.PasswordSalt;
            }
        }

        /// <summary>
        /// Gets the number of failed password attempts since the last successful login
        /// </summary>
        public byte FailedPasswordAttemptCount
        {
            get
            {
                return Data.FailedPasswordAttemptCount;
            }
        }

        #endregion

        #region Read/Write Properties

        /// <summary>
        /// Gets or sets the Username of the account
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if the value set is null or empty</exception>
        /// <exception cref="FormatException">Thrown if the value set is invalid for this field</exception>
        /// <exception cref="InvalidOperationException">Thrown if the account name would cause a duplicate</exception>
        /// <exception cref="IndexOutOfRangeException">Thrown if the length is more than 50 characters</exception>
        public string AccountName
        {
            get
            {
                return Data.AccountName;
            }

            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("value", "AccountName cannot be null or empty");
                }

                if (value.Length > 50)
                {
                    throw new IndexOutOfRangeException(string.Format("Name is more than 50 Characters. Currently: {0}", value.Length));
                }

                if (!IsAccountNameValid(value))
                {
                    throw new FormatException("AccountName requested is in an invalid format. Check with IsAccountNameValid first.");
                }

                if (Repository.AccountNameExists(value, ID))
                {
                    throw new InvalidOperationException("AccountName Already Exists");
                }

                Data.AccountName = value;
            }
        }

        /// <summary>
        /// Gets or sets the email address for the account
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if the value set is null or empty</exception>
        /// <exception cref="FormatException">Thrown if the value set is invalid for this field</exception>
        /// <exception cref="InvalidOperationException">Thrown if the email address would cause a duplicate</exception>
        public string EmailAddress
        {
            get
            {
                return Data.EmailAddress;
            }

            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("value", "EmailAddress cannot be null or empty");
                }

                if (!IsEmailAddressValid(value))
                {
                    throw new FormatException("EmailAddress requested is in an invalid format. Check with IsEmailAddressValid first.");
                }

                if (Repository.EmailAddressExists(value, ID))
                {
                    throw new InvalidOperationException("EmailAddress Already Exists");
                }

                Data.EmailAddress = value;
            }
        }

        /// <summary>
        /// Gets or sets the descriptive comment for the account
        /// </summary>
        public string Comment
        {
            get
            {
                return Data.Comment;
            }

            set
            {
                Data.Comment = value;
            }
        }

        /// <summary>
        /// Gets or sets the question for password recovery for the account
        /// </summary>
        public string RecoveryQuestion
        {
            get
            {
                return Data.RecoveryQuestion;
            }

            set
            {
                Data.RecoveryQuestion = value;
            }
        }

        /// <summary>
        /// Gets or sets the answer for password recovery for the account
        /// </summary>
        public string RecoveryAnswer
        {
            get
            {
                return Data.RecoveryAnswer;
            }

            set
            {
                Data.RecoveryAnswer = value;
            }
        }

        /// <summary>
        /// Gets a value indicating whether or not the account is currently banned
        /// </summary>
        public bool Banned
        {
            get
            {
                return Data.Banned ? true : Data.DateBannedUntil > DateTime.UtcNow;
            }
        }

        /// <summary>
        /// Gets a value indicating whether or not the account is currently locked
        /// </summary>
        public bool Locked
        {
            get
            {
                return Data.Locked ? true : Data.DateLockedUntil > DateTime.UtcNow;
            }
        }

        #endregion

        #region Public Static Factory Methods

        /// <summary>
        /// Retrieves a user by the email address or returns null if no account was found
        /// </summary>
        /// <param name="emailAddress">Email address to search by</param>
        /// <returns>Account found or null if no account was found</returns>
        /// <exception cref="ArgumentNullException">Thrown if EmailAddress is null</exception>
        public static Account RetrieveByEmailAddress(string emailAddress)
        {
            return RetrieveByEmailAddress(new RepositoryFactory(), emailAddress);
        }

        /// <summary>
        /// Retrieves a user by the account name or returns null if no account was found
        /// </summary>
        /// <param name="accountName">Account name to search by</param>
        /// <returns>Account found or null if no account was found</returns>
        /// <exception cref="ArgumentNullException">Thrown if accountName is null</exception>
        public static Account RetrieveByUserName(string accountName)
        {
            return RetrieveByUserName(new RepositoryFactory(), accountName);
        }

        /// <summary>
        /// Retrieves a user by the account name or returns null if no account was found
        /// </summary>
        /// <param name="accountName">Account name to search by</param>
        /// <returns>Boolean whether requested account exists in the system</returns>
        /// <exception cref="ArgumentNullException">Thrown if accountName is null</exception>
        public static bool AccountNameExists(string accountName)
        {
            return AccountNameExists(accountName, null);
        }

        /// <summary>
        /// Retrieves a user by the account name or returns null if no account was found
        /// </summary>
        /// <param name="accountName">Account name to search by</param>
        /// <param name="ignoreID">Identity to ignore during search (Optional)</param>
        /// <returns>Boolean whether requested account exists in the system</returns>
        /// <exception cref="ArgumentNullException">Thrown if accountName is null</exception>
        public static bool AccountNameExists(string accountName, Identity? ignoreID)
        {
            return AccountNameExists(new RepositoryFactory(), accountName, ignoreID);
        }

        /// <summary>
        /// Checks the data source to see if a specified email address exists with the option of ignoring a specified ID
        /// </summary>
        /// <param name="EmailAddress">Email Address to search by</param>
        /// <returns>Boolean whether requested account exists in the system</returns>
        /// <exception cref="ArgumentNullException">Thrown if EmailAddress is null</exception>
        public static bool EmailAddressExists(string EmailAddress)
        {
            return EmailAddressExists(new RepositoryFactory(), EmailAddress, null);
        }

        /// <summary>
        /// Checks the data source to see if a specified email address exists with the option of ignoring a specified ID
        /// </summary>
        /// <param name="EmailAddress">Email Address to search by</param>
        /// <param name="IgnoreID">Identity to ignore during search (Optional)</param>
        /// <returns>Boolean whether requested account exists in the system</returns>
        /// <exception cref="ArgumentNullException">Thrown if EmailAddress is null</exception>
        public static bool EmailAddressExists(string EmailAddress, Identity? IgnoreID)
        {
            return EmailAddressExists(new RepositoryFactory(), EmailAddress, IgnoreID);
        }

        #endregion

        #region Public Static Validation Methods

        /// <summary>
        /// Tests whether or not a specified account name is considered valid
        /// </summary>
        /// <param name="accountName">Account Name to validate</param>
        /// <returns>Boolean of whether or not the account is valid</returns>
        public static bool IsAccountNameValid(string accountName)
        {
            // First position must be alpha based, everything else can be alphanumeric
            Regex check = new Regex("^[a-zA-Z]{1}[a-zA-Z0-9]*$");

            return check.IsMatch(accountName);
        }

        /// <summary>
        /// Tests whether or not a specified Email Address is considered valid
        /// </summary>
        /// <param name="emailAddress">Email Address to validate</param>
        /// <returns>Boolean of whether or not the Email Address is valid</returns>
        public static bool IsEmailAddressValid(string emailAddress)
        {
            // RFC based Check
            Regex check = new Regex(@"^\w+((-\w+)|(\.\w+))*\@(([a-zA-Z][\w-]{0,61}[\w]+[\.]?)*|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))$");

            return check.IsMatch(emailAddress);
        }

        #endregion

        #region Public Validations

        /// <summary>
        /// Validates the Account is valid
        /// </summary>
        /// <returns>Boolean based on the validity of the object</returns>
        public override bool IsValid()
        {
            // Check required fields
            if (string.IsNullOrEmpty(Data.AccountName))
            {
                return false;
            }

            if (string.IsNullOrEmpty(Data.EmailAddress))
            {
                return false;
            }

            if (Data.Password == null || Data.Password.Length <= 0)
            {
                return false;
            }

            if (Data.PasswordSalt == null || Data.PasswordSalt.Length <= 0)
            {
                return false;
            }

            // Check the Values
            if (!IsAccountNameValid(Data.AccountName))
            {
                return false;
            }

            if (!IsEmailAddressValid(Data.EmailAddress))
            {
                return false;
            }

            // Everything passed by this point!
            return true;
        }

        #endregion

        #region Password Management

        /// <summary>
        /// Regenerates the salt for the user
        /// </summary>
        /// <remarks>Make sure that a new password is created right after to be able to validate it</remarks>
        public void GenerateNewSalt()
        {
            // Allocate a byte array, which will hold the salt.
            byte[] saltBytes = new byte[256];

            // Fill the salt with cryptographically strong byte values.
            (new RNGCryptoServiceProvider()).GetNonZeroBytes(saltBytes);

            // Set it to the password salt field
            Data.PasswordSalt = saltBytes;
        }

        /// <summary>
        /// Sets a new password for the account from the passed in value
        /// </summary>
        /// <param name="newPassword">New password to set the account's password to</param>
        public void SetPassword(string newPassword)
        {
            if (string.IsNullOrEmpty(newPassword))
            {
                throw new ArgumentNullException("newPassword", "Account must have a password");
            }

            List<byte> passwordData = new List<byte>();

            passwordData.AddRange(Encoding.Unicode.GetBytes(newPassword));
            passwordData.AddRange(Data.PasswordSalt);

            Data.Password = HashAlgorithm.Create("SHA512").ComputeHash(passwordData.ToArray());
            Data.DateLastPasswordChanged = DateTime.UtcNow;
        }

        /// <summary>
        /// Trys to validate the password for the user using the specified password
        /// </summary>
        /// <param name="password">Password to check against</param>
        /// <returns>Boolean of whether or not the validation was successful</returns>
        public bool ValidatePassword(string password)
        {
            List<byte> passwordData = new List<byte>();

            passwordData.AddRange(Encoding.Unicode.GetBytes(password));
            passwordData.AddRange(Data.PasswordSalt);

            // This is all there is to validate!
            if (!(Encoding.Unicode.GetString(Data.Password) == Encoding.Unicode.GetString(HashAlgorithm.Create("SHA512").ComputeHash(passwordData.ToArray()))))
            {
                Data.FailedPasswordAttemptCount++;
                return false;
            }

            return true;
        }

        /// <summary>
        /// Trys to validate the password for the user using the 2 stage hash
        /// </summary>
        /// <param name="verifyHash">Hash generated on the client side of the ((password, salt), additionalData)</param>
        /// <param name="additionalData">Additional data used for the second hash stage</param>
        /// <returns>Boolean of whether or not the validation was successful</returns>
        public bool ValidatePassword(byte[] verifyHash, byte[] additionalData)
        {
            List<byte> passwordData = new List<byte>();

            passwordData.AddRange(additionalData);
            passwordData.AddRange(Data.Password);

            // This is all there is to validate!
            if (!(Encoding.Unicode.GetString(verifyHash) == Encoding.Unicode.GetString(HashAlgorithm.Create("SHA512").ComputeHash(passwordData.ToArray()))))
            {
                Data.FailedPasswordAttemptCount++;
                return false;
            }

            return true;
        }

        #endregion

        #region Public Methods

        #region Reset methods

        /// <summary>
        /// Resets the counter for the invalid passwords
        /// </summary>
        public void ResetInvalidPasswordAttempts()
        {
            Data.FailedPasswordAttemptCount = 0;
        }

        /// <summary>
        /// Resets the date the account was last locked out. Does not unlock the account.
        /// </summary>
        public void ResetLockoutDate()
        {
            Data.DateLastLocked = null;
        }

        /// <summary>
        /// Resets the date the account was last banned. Does not unban the account.
        /// </summary>
        public void ResetBannedDate()
        {
            Data.DateLastBanned = null;
        }

        /// <summary>
        /// Resets the date the account was last logged in.
        /// </summary>
        public void ResetLoginDate()
        {
            Data.DateLastLogin = null;
        }

        /// <summary>
        /// Resets the date the last password change was made
        /// </summary>
        public void ResetDateLastPasswordChanged()
        {
            Data.DateLastPasswordChanged = null;
        }

        #endregion

        #region Lockout/Ban

        /// <summary>
        /// Locks the account until the specified date
        /// </summary>
        /// <param name="lockUntil">Date to lock until</param>
        public void LockAccount(DateTime lockUntil)
        {
            Data.DateLockedUntil = lockUntil;
            Data.DateLastLocked = DateTime.UtcNow;
        }

        /// <summary>
        /// Locks the account until further notice
        /// </summary>
        public void LockAccount()
        {
            Data.Locked = true;
            Data.DateLastLocked = DateTime.UtcNow;
        }

        /// <summary>
        /// Unlocks the account by resetting the lock until date and futher notice
        /// </summary>
        public void UnlockAccount()
        {
            Data.Locked = false;
            Data.DateLockedUntil = null;
        }

        /// <summary>
        /// Bans the account until the specified date
        /// </summary>
        /// <param name="banUntil">Date to ban until</param>
        public void BanAccount(DateTime banUntil)
        {
            Data.DateBannedUntil = banUntil;
            Data.DateLastBanned = DateTime.UtcNow;
        }

        /// <summary>
        /// Bans the account until further notice
        /// </summary>
        public void BanAccount()
        {
            Data.Banned = true;
            Data.DateLastBanned = DateTime.UtcNow;
        }

        /// <summary>
        /// Unbans the account by resetting the ban until date and futher notice
        /// </summary>
        public void UnbanAccount()
        {
            Data.Banned = false;
            Data.DateBannedUntil = null;
        }

        #endregion

        /// <summary>
        /// Resets the date the account was last locked out. Does not unlock the account.
        /// </summary>
        public void UpdateLoginDate()
        {
            Data.DateLastLogin = DateTime.UtcNow;
        }

        #endregion

        #region Internal Static Factory Methods

        #region RetrieveByUserName

        /// <summary>
        /// Retrieves a user by the account name or returns null if no account was found
        /// </summary>
        /// <param name="factory">Factory to use to retrieve data from</param>
        /// <param name="accountName">Account name to search by</param>
        /// <returns>Account found or null if no account was found</returns>
        /// <exception cref="ArgumentNullException">Thrown if either factory or accountName are null</exception>
        internal static Account RetrieveByUserName(IRepositoryFactory factory, string accountName)
        {
            if (factory == null)
            {
                throw new ArgumentNullException("factory", "Factory cannot be null to retrieve data");
            }

            if (string.IsNullOrEmpty(accountName))
            {
                throw new ArgumentNullException("accountName", "Must have an account name to search by");
            }

            // Try to get an account
            IAccountData account = RetrieveRepository(factory).RetrieveByUserName(accountName);

            // If an account was found then cast it into a Account
            return account != null ? new Account(factory, account) : null;
        }

        #endregion

        #region RetrieveByEmailAddress

        /// <summary>
        /// Retrieves a user by the email address or returns null if no account was found
        /// </summary>
        /// <param name="factory">Factory to use to retrieve data from</param>
        /// <param name="emailAddress">Email address to search by</param>
        /// <returns>Account found or null if no account was found</returns>
        /// <exception cref="ArgumentNullException">Thrown if either factory or emailAddress are null</exception>
        internal static Account RetrieveByEmailAddress(IRepositoryFactory factory, string emailAddress)
        {
            if (factory == null)
            {
                throw new ArgumentNullException("factory", "Factory cannot be null to retrieve data");
            }

            if (string.IsNullOrEmpty(emailAddress))
            {
                throw new ArgumentNullException("emailAddress", "Must have an email address to search by");
            }

            // Try to get an account
            IAccountData account = RetrieveRepository(factory).RetrieveByEmailAddress(emailAddress);

            // If an account was found then cast it into a Account
            return account != null ? new Account(factory, account) : null;
        }

        #endregion

        #region AccountNameExists

        /// <summary>
        /// Retrieves a user by the account name or returns null if no account was found
        /// </summary>
        /// <param name="factory">Factory to use to retrieve data from</param>
        /// <param name="accountName">Account name to search by</param>
        /// <param name="ignoreID">Identity to ignore during search (Optional)</param>
        /// <returns>Boolean whether requested account exists in the system</returns>
        /// <exception cref="ArgumentNullException">Thrown if either factory or accountName are null</exception>
        internal static bool AccountNameExists(IRepositoryFactory factory, string accountName, Identity? ignoreID)
        {
            if (factory == null)
            {
                throw new ArgumentNullException("factory", "Factory cannot be null to retrieve data");
            }

            if (string.IsNullOrEmpty(accountName))
            {
                throw new ArgumentNullException("accountName", "Must have an account name to search by");
            }

            return RetrieveRepository(factory).AccountNameExists(accountName, ignoreID);
        }

        #endregion

        #region EmailAddressExists

        /// <summary>
        /// Checks the data source to see if a specified email address exists with the option of ignoring a specified ID
        /// </summary>
        /// <param name="factory">Factory to use to retrieve data from</param>
        /// <param name="emailAddress">Email Address to search by</param>
        /// <param name="ignoreID">Identity to ignore during search (Optional)</param>
        /// <returns>Boolean whether requested account exists in the system</returns>
        /// <exception cref="ArgumentNullException">Thrown if either factory or emailAddress are null</exception>
        internal static bool EmailAddressExists(IRepositoryFactory factory, string emailAddress, Identity? ignoreID)
        {
            if (factory == null)
            {
                throw new ArgumentNullException("factory", "Factory cannot be null to retrieve data");
            }

            if (string.IsNullOrEmpty(emailAddress))
            {
                throw new ArgumentNullException("emailAddress", "Must have an email address to search by");
            }

            return RetrieveRepository(factory).EmailAddressExists(emailAddress, ignoreID);
        }

        #endregion

        #endregion
    }
}